Skip to content

test(e2e): guard destructive spend-log truncate behind an explicit opt-in - #33751

Merged
yassin-berriai merged 1 commit into
litellm_internal_stagingfrom
litellm_lit4555_e2e_spend_truncate_guard
Jul 20, 2026
Merged

test(e2e): guard destructive spend-log truncate behind an explicit opt-in#33751
yassin-berriai merged 1 commit into
litellm_internal_stagingfrom
litellm_lit4555_e2e_spend_truncate_guard

Conversation

@yassin-berriai

@yassin-berriai yassin-berriai commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Relevant issues

Linear ticket

Resolves LIT-4555

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have added meaningful tests
  • My PR passes all CI/CD checks (e.g., lint, format, unit tests)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review (Greptile reviews automatically once the PR is opened; only comment @greptileai to re-request a review after pushing changes)

Screenshots / Proof of Fix

Harness-internal change with no live-proxy HTTP surface, so the proof drives the exact production functions the session-finish hook calls (run_spend_log_cleanup and reset_spend_logs) against a real Postgres seeded with spend rows. No mocks; the truncate hits real LiteLLM_SpendLogs rows

### BEFORE THE FIX (old hook truncated once any e2e test ran, no opt-in) ###
  rows before: 3
  rows after:  0   <-- real spend data WIPED with no opt-in

### AFTER THE FIX, no opt-in set, an e2e test ran ###
  rows before: 3
  truncated? False
  rows after:  3   <-- PRESERVED

### AFTER THE FIX, explicit E2E_RESET_SPEND_LOGS=1, an e2e test ran ###
  rows before: 3
  truncated? True
  rows after:  0   <-- reset ran, opt-in honored

The middle case is the bug this closes: a routine local run that merely exercised a test, pointed at a shared or staging DATABASE_URL, no longer wipes real spend data. The truncate now happens only on the explicit opt-in

Harness gates, all green on this branch:

make lint-e2e-basedpyright            -> 0 errors, 0 warnings, 0 notes
coverage_registry.collector --strict  -> exit 0 (no markers outside the registry)
pytest --collect-only tests/e2e       -> collects with no new import/fixture errors

Type

🧹 Refactoring

Changes

tests/e2e/conftest.py's pytest_sessionfinish truncated LiteLLM_SpendLogs against whatever DATABASE_URL resolved to, gated only by "an e2e test body ran". Pointed at a shared or staging DB, a routine local run wiped real spend data. It also reached the truncate helper through a sys.path.insert into quota_management/spend_tracking/spend_e2e_client.py, a cross-suite import-by-path hack it then unwound in a finally

The cleanup now routes through a new run_spend_log_cleanup in a top-level tests/e2e/e2e_db.py, which fires the destructive truncate only when the operator set E2E_RESET_SPEND_LOGS=1 and an e2e test actually ran. Any other value (unset, 0, true, empty) leaves the DB untouched, so presence of the variable alone or a test run alone never arms the truncate. The decision plus the injectable truncate callable live in that pure helper, and conftest is a thin adapter that supplies os.environ.get(...), the session stash, and reset_spend_logs

reset_spend_logs itself moved from spend_e2e_client.py into e2e_db.py (implementation unchanged: same TRUNCATE TABLE "LiteLLM_SpendLogs", same DATABASE_URL default). It now sits next to e2e_config and lifecycle, so both the top-level conftest and any suite import it by name; the sys.path munging is gone. Nothing else in the repo imported reset_spend_logs, so spend_e2e_client.py drops the definition, its __all__ entry, and the now-unused os import with no other caller affected

QA runbook

This PR edits tests/e2e but adds no e2e-marked tests, and the only behavior change is in the session-finish cleanup, which has no request surface. So there is no live-proxy reproduction. To verify the guard by hand against a local Postgres holding spend rows:

  • Seed LiteLLM_SpendLogs with a few rows on the DB your DATABASE_URL points at
  • With E2E_RESET_SPEND_LOGS unset, run an e2e session that touches a live proxy (pytest -m e2e tests/e2e/quota_management/spend_tracking/) and confirm the seeded rows are still present afterward
  • Re-seed, export E2E_RESET_SPEND_LOGS=1, run the same session, and confirm LiteLLM_SpendLogs is empty afterward

Final Attestation

  • The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR

Link to Devin session: https://app.devin.ai/sessions/2b24353a94a44e6998e91f09c67fa117
Requested by: @yassin-berriai

@gemini-code-assist

Copy link
Copy Markdown

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@yassin-berriai

Copy link
Copy Markdown
Contributor Author

@greptileai

@yassin-berriai

Copy link
Copy Markdown
Contributor Author

osv-scan is red from base drift, not this PR. It flags mcp==1.26.0 in uv.lock (GHSA-hvrp-rf83-w775, GHSA-jpw9-pfvf-9f58, GHSA-vj7q-gjh5-988w; fixed in 1.27.2/1.28.1). This branch is a tests/e2e-only change and touches no dependency or lock files (git diff origin/litellm_internal_staging..HEAD lists no uv.lock/pyproject.toml), and the same osv-scan failure is present on every currently open PR. The mcp bump belongs in its own dependency PR on the base branch

@greptile-apps

greptile-apps Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR guards the destructive LiteLLM_SpendLogs truncation behind an explicit E2E_RESET_SPEND_LOGS=1 opt-in, so a routine local or CI run that merely exercises an e2e test against a shared DATABASE_URL can no longer wipe real spend data.

  • tests/e2e/e2e_db.py is introduced as the single source of truth for destructive DB helpers; run_spend_log_cleanup requires both the env-var opt-in and a confirmed e2e test run before delegating to reset_spend_logs.
  • conftest.pytest_sessionfinish is reduced to a one-call adapter that supplies os.environ.get(...), the session stash flag, and the truncate callable — the sys.path mutation and dynamic import are gone.
  • reset_spend_logs is moved from spend_e2e_client.py to e2e_db.py (implementation unchanged); the original module drops the definition, its __all__ entry, and the now-unused os import.

Confidence Score: 5/5

Safe to merge — changes are confined to the e2e test harness, add no new network calls, and the only behavioral shift (requiring an explicit opt-in before truncating spend logs) is strictly safer than the prior default-truncate behavior.

The destructive truncate is now correctly double-gated: the guard in run_spend_log_cleanup requires opt_in == "1" AND e2e_test_ran, and the old sys.path mutation is gone. The implementation in e2e_db.py is a clean extraction of logic that was already proven correct in spend_e2e_client.py. No production code is touched.

No files require special attention.

Important Files Changed

Filename Overview
tests/e2e/e2e_db.py New helper module: run_spend_log_cleanup gates the destructive truncate on opt_in == "1" AND e2e_test_ran; reset_spend_logs moved here from spend_e2e_client.py unchanged
tests/e2e/conftest.py pytest_sessionfinish simplified to a single call to run_spend_log_cleanup; sys.path mutation and dynamic import removed; no other hooks changed
tests/e2e/quota_management/spend_tracking/spend_e2e_client.py reset_spend_logs and its unused os import removed from all and from the module; no other callers affected

Reviews (4): Last reviewed commit: "test(e2e): guard destructive spend-log t..." | Re-trigger Greptile

Comment thread tests/e2e/test_e2e_db.py Outdated
@greptile-apps

greptile-apps Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR guards the destructive LiteLLM_SpendLogs truncation behind an explicit E2E_RESET_SPEND_LOGS=1 opt-in, preventing routine e2e runs from wiping spend data on shared or staging databases. It also eliminates the sys.path hack by moving reset_spend_logs into a top-level e2e_db.py module and unit-testing the gate logic without a live DB.

  • run_spend_log_cleanup in e2e_db.py requires both opt_in == \"1\" and e2e_test_ran == True; test_e2e_db.py covers all seven opt-in/test-ran combinations with a spy callable.
  • The refactor removes the early return in pytest_sessionfinish that previously also guarded the bob_the_builder.remediate(session) call; remediate now runs unconditionally on every session finish, not only when an e2e-marked test ran.
  • run_spend_log_cleanup returns True whether truncate() succeeded or raised, so callers cannot distinguish a successful truncation from a silently-failed one.

Confidence Score: 3/5

Safe to merge for the spend-log guard itself; the unintended un-gating of bob_the_builder.remediate needs a second look before landing.

The core opt-in guard is correct and well-tested. The main concern is in conftest.py: removing the early return means bob_the_builder.remediate(session) now runs at the end of every session, including pure harness-unit runs where no e2e-marked test executed. If remediate has external side effects, they will fire unexpectedly on unit-only runs.

tests/e2e/conftest.py — specifically whether bob_the_builder.remediate should remain behind the 'an e2e test ran' guard.

Important Files Changed

Filename Overview
tests/e2e/conftest.py Spend-log cleanup delegated to run_spend_log_cleanup; however removing the early return also un-gates bob_the_builder.remediate, which previously only ran when an e2e test had executed.
tests/e2e/e2e_db.py New module housing the opt-in guard and the moved reset_spend_logs implementation; logic is correct but run_spend_log_cleanup returns True whether truncate() succeeded or failed.
tests/e2e/quota_management/spend_tracking/spend_e2e_client.py Cleaned up: reset_spend_logs, its __all__ entry, and the now-unused os import removed; no other callers affected.
tests/e2e/test_e2e_db.py New unit tests covering all opt-in/test-ran combinations and the best-effort exception swallow; no DB or proxy needed to run.

Comments Outside Diff (1)

  1. tests/e2e/conftest.py, line 120-131 (link)

    P1 bob_the_builder.remediate now runs unconditionally

    The old pytest_sessionfinish opened with if not session.stash.get(_E2E_TEST_RAN, False): return, which guarded both the spend-log cleanup and the subsequent bob_the_builder.remediate(session) call. The refactor moved the guard into run_spend_log_cleanup, but the early return was removed entirely, so remediate is now invoked at the end of every session — including pure unit runs (pytest tests/e2e/test_e2e_db.py) where no e2e-marked test ever ran. If remediate has side effects when called on an empty session (filing tickets, posting results, triggering CI actions), those will now fire on every harness-unit run. The PR description does not mention this as an intentional change.

Reviews (1): Last reviewed commit: "test(e2e): guard destructive spend-log t..." | Re-trigger Greptile

Comment thread tests/e2e/e2e_db.py
@codecov

codecov Bot commented Jul 17, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@yassin-berriai

Copy link
Copy Markdown
Contributor Author

Addressed the P1 in 0f1d45f. You're right that dropping the early return un-gated bob_the_builder.remediate. Restored its original gating: pytest_sessionfinish now reads e2e_test_ran once, passes it to run_spend_log_cleanup (which already no-ops when it's false), and returns before the remediate block when no e2e test ran, so remediate keeps running only on real e2e sessions exactly as before. Also strengthened test_cleanup_swallows_truncate_failure to assert the injected truncate was actually invoked (calls == 1) when it raises, not just that the return value is True.

@greptileai please review the current head 0f1d45f

@yassin-berriai

Copy link
Copy Markdown
Contributor Author

The misc / Run tests red is a pre-existing failure on the base branch, not this PR. It's tests/test_litellm/test_gpt_realtime_mode.py::test_get_model_info_reports_realtime_mode failing with assert 'chat' == 'realtime'; that test fails identically when run on litellm_internal_staging at its current tip (get_model_info('gpt-realtime-mini')['mode'] returns chat, not realtime). This branch is a tests/e2e-only change (git diff origin/litellm_internal_staging..HEAD lists nothing outside tests/e2e/), so it cannot affect that model-info assertion; CI evaluates the PR merged into staging, which is how the base breakage surfaces here. It belongs in a separate fix on the base branch

@codspeed-hq

codspeed-hq Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing litellm_lit4555_e2e_spend_truncate_guard (a2636fa) with litellm_internal_staging (e238e89)

Open in CodSpeed

…t-in

tests/e2e/conftest.py's pytest_sessionfinish truncated LiteLLM_SpendLogs
against whatever DATABASE_URL resolved to, gated only by "an e2e test body
ran". Pointed at a shared or staging DB, a routine local run wiped real
spend data. It also reached the truncate helper through a sys.path.insert
into quota_management/spend_tracking/spend_e2e_client.py, a cross-suite
import-by-path hack it then unwound in a finally.

The cleanup now routes through a new run_spend_log_cleanup in a top-level
tests/e2e/e2e_db.py, which fires the destructive truncate only when the
operator set E2E_RESET_SPEND_LOGS=1 and an e2e test actually ran. Any other
value (unset, 0, true, empty) leaves the DB untouched, so presence of the
variable alone or a test run alone never arms the truncate. The decision
plus the injectable truncate callable live in that pure helper, and conftest
is a thin adapter that supplies os.environ.get(...), the session stash, and
reset_spend_logs.

reset_spend_logs itself moved from spend_e2e_client.py into e2e_db.py
(implementation unchanged), sitting next to e2e_config and lifecycle so both
conftest and any suite import it by name; the sys.path munging is gone.
Nothing else imported reset_spend_logs, so spend_e2e_client.py drops the
definition, its __all__ entry, and the now-unused os import.
@yassin-berriai
yassin-berriai force-pushed the litellm_lit4555_e2e_spend_truncate_guard branch from cf04467 to a2636fa Compare July 18, 2026 21:26
@yassin-berriai
yassin-berriai enabled auto-merge (squash) July 18, 2026 21:26
@yassin-berriai

Copy link
Copy Markdown
Contributor Author

@greptileai please review the current head a2636fa — the branch was rebased onto current staging and the spend-log truncate guard was reapplied as a single commit; the prior review is pinned to a now-rewritten commit

@yassin-berriai
yassin-berriai disabled auto-merge July 20, 2026 15:47
@yassin-berriai
yassin-berriai merged commit 8d96e95 into litellm_internal_staging Jul 20, 2026
79 checks passed
@yassin-berriai
yassin-berriai deleted the litellm_lit4555_e2e_spend_truncate_guard branch July 20, 2026 15:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants